001.LlamaIndex 构建结构化检索

一:核心概念与痛点分析

1.1. 结构化数据 vs 非结构化数据

维度 非结构化数据 RAG 结构化数据 RAG
数据形式 文档、PDF、网页 数据库表、CSV、Excel
查询示例 "解释量子计算" "过去 30 天销售额?"
检索方式 向量相似度 SQL 查询 + 向量检索
核心挑战 语义理解 精确计算、聚合
主要技术 Embedding + 向量库 Text-to-SQL + 混合检索

核心痛点

  1. 向量检索无法精确聚合 - "过去 30 天销售额"无法通过向量计算
  2. Text-to-SQL 缺乏语义理解 - "推荐适合的产品" SQL 无法理解
  3. 单一方案覆盖不全 - 同时需要精确计算和语义理解
  4. 性能与成本平衡难 - 准确率、速度、成本的三角矛盾

1. 结构化数据与语义向量空间不匹配

大多数 RAG 实现依赖向量检索技术(embedding + 向量数据库)对文本进行相似度搜索。这对非结构化文本有效,但对表格 / 结构化数据效果差强人意:

这类问题直接影响检索质量与后续生成的准确性,是结构化数据 RAG 最大的难点之一。

2. 查询语义对结构化数据的不匹配

用户往往以自然语言提问(例如 “过去三年中欧盟 vs 美国赢得的法律案件数对比?”),而结构化数据一般需要通过 SQL 或逻辑分析表达:

这会导致检索结果不准确或可执行查询失败。

3. 数据规模与实时性压力

企业结构化数据量通常很大,并且更新频繁:

这就要求复杂的数据管道和高性能基础设施。

4. 复杂数据预处理与特征提取成本高

为让结构化数据适配 RAG,需要很多额外处理:

这些预处理步骤往往需要 ML 专业知识与工程投入。

下表总结结构化数据在 RAG 过程中的关键难点:

难点 本质问题 对系统影响
向量化语义丢失 向量 embedding 无法表示复杂表格关系 检索结果不准确
自然语言查询转换困难 语言与关系模型语义不匹配 生成错误 SQL 或无效查询
大规模实时更新 数据同步与索引维护压力大 延迟高、结果不及时
结构丢失与逻辑混乱 扁平文本丧失字段间依赖关系 回答错误、高幻觉
数据预处理与工程复杂 需要大量特征工程与 schema 建模 增加开发成本

在企业 RAG 落地中,结构化数据之所以成为“硬骨头”,是因为它试图用概率性的模型(LLM / 向量)去解决确定性的逻辑问题(SQL / 计算)。

核心结论与建议

结构化数据 RAG 的关键不在“模型能力”,而在“系统设计能力”


二:环境准备

# 安装依赖
pip install -q llama-index-core llama-index-llms-openai llama-index-embeddings-openai llama-index-llms-dashscope llama-index-embeddings-dashscope
pip install -q llama-index-vector-stores-milvus llama-index-retrievers-bm25 rank-bm25 llama-parse
pip install -q sqlalchemy pandas python-dotenv 

print("✅ 依赖安装完成")
# 全局配置
import os
import warnings
from dotenv import load_dotenv
from llama_index.llms.dashscope import DashScope
from llama_index.embeddings.dashscope import DashScopeEmbedding
from llama_index.core.settings import Settings

warnings.filterwarnings('ignore')
# 加载环境变量
load_dotenv(override=True)

# 设置 LLM
Settings.llm = DashScope(
    model_name="qwen-max",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    api_base=os.getenv("DASHSCOPE_BASE_URL", "https://api.dashscope.aliyuncs.com")
)

# 设置 Embedding模型
Settings.embed_model = DashScopeEmbedding(
    model_name="text-embedding-v4",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    api_base=os.getenv("DASHSCOPE_BASE_URL", "https://api.dashscope.aliyuncs.com")
)
# 基本使用
response = Settings.llm.complete("你好,请介绍一下通义千问。")
print(response)

response_embed = Settings.embed_model.get_query_embedding("你好,请介绍一下通义千问。")
print(response_embed)

2.1. Markdown 文档中的表格检索方式

1. 数据解析 (Parsing Phase)

import os
import pandas as pd
from llama_index.core import Document, VectorStoreIndex
from llama_index.core.node_parser import MarkdownElementNodeParser
from llama_index.core.retrievers import RecursiveRetriever
from llama_index.llms.openai import OpenAI
from llama_index.core.schema import TextNode, IndexNode # 确保导入这两个类

# 0. 准备测试数据 (确保表格格式标准)
markdown_content = """
# 公司季度财报
以下是我们在2023年第三季度的详细财务表现:
| 部门 | 收入(万元) | 成本(万元) | 利润(万元) |
|------|------------|------------|------------|
| 研发部 | 0 | 500 | -500 |
| 销售部 | 2000 | 800 | 1200 |
| 产品部 | 500 | 200 | 300 |
总结来看,销售部表现最为亮眼。
"""
doc = Document(text=markdown_content)

# 1. 创建解析器
node_parser = MarkdownElementNodeParser(
    llm=Settings.llm, 
    num_workers=1   # 并行解析数
)

# 2. 正确的解析流程
# 第一步: 从文档获取原始节点
raw_nodes = node_parser.get_nodes_from_documents([doc])

# 第二步: 从节点中提取基座节点和表格对象
base_nodes, objects = node_parser.get_nodes_and_objects(raw_nodes)
print(f"解析完成: 生成了 {len(base_nodes)} 个基座节点, {len(objects)} 个表格对象")

# 3. 验证表格对象
print(f"\nobjects 类型: {type(objects)}")

if objects:
    for i, obj in enumerate(objects):
        print(f"  对象 {i}:")
        print(f"    类型: {type(obj).__name__}")
        print(f"    ID: {obj.node_id if hasattr(obj, 'node_id') else 'N/A'}")
        # 如果是表格节点,尝试显示内容
        if hasattr(obj, 'text'):
            print(f"    内容预览: {obj.text[:100]}...")
        if hasattr(obj, 'metadata'):
            print(f"    元数据: {obj.metadata}")
else:
    print("\n⚠️ 未提取到表格,调试信息:")
    for i, node in enumerate(raw_nodes):
        print(f"  节点 {i}: {type(node).__name__} - {node.text[:60]}...")

2. 检索与生成 (Retrieval & Generation Phase)

# 4. 过滤 base_nodes (只保留 TextNode 和 IndexNode)
base_nodes_filtered = [n for n in raw_nodes if isinstance(n, (TextNode, IndexNode))]
print(f"\n过滤后基座节点数: {len(base_nodes_filtered)}")

# 5. 建立向量索引 (只对摘要建索引)
vector_index = VectorStoreIndex(nodes=base_nodes_filtered)
vector_retriever = vector_index.as_retriever(similarity_top_k=2)

# 6. 构建递归检索器
# 将 objects 列表转换为字典 {node_id: node}
objects_dict = {obj.node_id: obj for obj in objects}

recursive_retriever = RecursiveRetriever(
    "vector",
    retriever_dict={"vector": vector_retriever},
    node_dict=objects_dict,  # 传入表格对象字典
    verbose=True  # 开启日志,可以看到递归过程
)

# 7. 执行检索测试
query = "销售部的利润是多少?"

retrieved_nodes = recursive_retriever.retrieve(query)

for i, node_with_score in enumerate(retrieved_nodes):
    print(f"\n--- 结果 {i+1} (相关度: {node_with_score.score:.3f}) ---")
    print(f"节点类型: {type(node_with_score.node).__name__}")
    print(f"内容:\n{node_with_score.node.text[:300]}...")
    
# 8. 结合 LLM 生成最终答案
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core import Settings

query_engine = RetrieverQueryEngine.from_args(
    recursive_retriever,
    llm=Settings.llm
)

response = query_engine.query(query)
print(f"\n问题: {query}")
print(f"\n答案: {response}")
print(f"\n来源节点数: {len(response.source_nodes)}")

2.2. PDF 文档中表格解析检索

重要说明

  1. LlamaParse API Key 需要在 https://cloud.llamaindex.ai 注册账号 免费版有每日解析页数限制(通常 1000 页 / 天) 将 API Key 替换代码中的 "llx-..."
  2. 成本优化 LlamaParse 是付费服务,如果文档较大建议先测试几页,可以通过 pages=[1, 2, 3] 参数只解析指定页
  3. 备选方案 (如果不想用 LlamaParse) 使用开源工具如 pdfplumber 或 camelot 提取表格,然后手工转换为 Markdown 但这种方式对复杂表格(跨行跨列)的处理效果通常不如 LlamaParse
import os
from llama_index.core import Document, VectorStoreIndex, Settings
from llama_index.core.node_parser import MarkdownElementNodeParser
from llama_index.core.retrievers import RecursiveRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.schema import TextNode, IndexNode
from llama_parse import LlamaParse
from dotenv import load_dotenv

load_dotenv(override=True)

# ========== 步骤 1: 使用 LlamaParse 解析 PDF ==========
parser = LlamaParse(
    result_type="markdown",  # 输出格式为 Markdown (保留表格结构)
    verbose=True,
    language="ch_sim",  # 简体中文
    # 可选: 提供自定义的解析指令
    # parsing_instruction="重点提取财务数据表格,保留表头和数值"
)

# 解析 PDF 文件
pdf_path = "dataset/招股意向书.pdf"
# 添加错误处理
try:
    documents = parser.load_data(pdf_path)
    
    # 检查是否为空
    if not documents:
        print("⚠️ 警告: 解析结果为空!")
        print("可能的原因:")
        print("1. PDF 文件路径不正确")
        print("2. LlamaParse API 调用失败")
        print("3. API Key 未正确配置")
        print(f"\n请检查:")
        print(f"- 文件是否存在: {os.path.exists(pdf_path)}")
        print(f"- API Key 是否已设置: {bool(os.getenv('LLAMA_CLOUD_API_KEY'))}")
    else:
        print(f"预览第一个片段:\n{documents[0].text[:300]}...\n")
        
except Exception as e:
    print(f"❌ PDF 解析失败: {e}")
    print("\n调试信息:")
    print(f"- 文件路径: {pdf_path}")
    print(f"- 文件存在: {os.path.exists(pdf_path)}")
    print(f"- API Key 已设置: {bool(os.getenv('LLAMA_CLOUD_API_KEY'))}")
    raise
# ========== 步骤 2: 使用 MarkdownElementNodeParser 提取表格 ==========
node_parser = MarkdownElementNodeParser(
    llm=Settings.llm,  # 用于生成表格摘要
    num_workers=4      # 并行处理
)

# 从文档获取原始节点
raw_nodes = node_parser.get_nodes_from_documents(documents)

# 提取基座节点和表格对象
base_nodes, objects = node_parser.get_nodes_and_objects(raw_nodes)

# 调试:查看提取到的表格
if objects:
    for i, obj in enumerate(objects[:3]):  # 只显示前 3 个
        print(f"  表格 {i+1}:")
        print(f"    ID: {obj.node_id}")
        print(f"    内容预览: {obj.text[:100]}...\n")

# ========== 步骤 3: 构建索引和递归检索器 ==========
# 过滤节点(只保留 TextNode 和 IndexNode)
base_nodes_filtered = [n for n in base_nodes if isinstance(n, (TextNode, IndexNode))]

# 建立向量索引
vector_index = VectorStoreIndex(nodes=base_nodes_filtered)
vector_retriever = vector_index.as_retriever(similarity_top_k=5)

# 构建 objects 字典
objects_dict = {obj.node_id: obj for obj in objects}

# 构建递归检索器
recursive_retriever = RecursiveRetriever(
    "vector",
    retriever_dict={"vector": vector_retriever},
    node_dict=objects_dict,
    verbose=True
)

# 构建查询引擎
query_engine = RetrieverQueryEngine.from_args(
    recursive_retriever,
    llm=Settings.llm
)
# ========== 步骤 4: 测试查询 ==========
# 示例查询(根据招股意向书的实际内容调整)
test_queries = [
    "体外诊断项目中,“输血项目”的检测范围和用途是什么?",
]
for query in test_queries:
    print(f"\n问题: {query}")
    response = query_engine.query(query)
    print(f"答案: {response}\n")
    print("-" * 60)

三:PandasQueryEngine - 小数据量高效方案

在处理小于 1 万行的结构化数据时,PandasQueryEngine 是最直接高效的解决方案:

3.1. 入门:基础查询

import pandas as pd
import time
from typing import Dict, Any, List
from llama_index.experimental.query_engine import PandasQueryEngine
from llama_index.llms.openai import OpenAI
from llama_index.core import Settings

import logging
# 禁用 Phoenix/OpenInference 的错误日志,在后续有提到的监控工具
logging.getLogger("openinference.instrumentation.llama_index").setLevel(logging.CRITICAL)
# 加载产品数据
df = pd.read_csv("dataset/products.csv")
df.head()

数据预览:

Pasted image 20260730145927.png

1. 创建 PandasQueryEngine

PandasQueryEngine 的核心参数:

参数 说明
df 要查询的 DataFrame
verbose 是否显示生成的 Pandas 代码
instruction_str 指导 LLM 如何生成代码
synthesize_response 是否用 LLM 综合最终响应
# 优化的指令字符串 - 关键配置
instruction_str = (
    "1. 将查询转换为可执行的 Pandas 代码 \n"
    "2. 代码必须是单行表达式,可以被 eval() 执行 \n"
    "3. DataFrame 变量名为 'df' \n"
    "4. 只输出表达式本身,不要有其他文字 \n"
    "5. 不要使用引号包裹表达式 \n"
    "\n 示例: \n"
    "问题: 找出价格最高的产品 \n"
    "代码: df.loc[df['price'].idxmax()]['name'] \n"
)

# 创建查询引擎
query_engine = PandasQueryEngine(
    df=df,   # DataFrame 读取的数据
    verbose=True,  # 显示生成的代码
    instruction_str=instruction_str,
    synthesize_response=True  # 综合回答
)
# 场景 1: 简单聚合查询
query = "电子产品类别有多少个产品?"
print(f"📝 查询: {query}")

response = query_engine.query(query)
print(f"💬 回答: {response}") # 💬 回答: 根据查询结果,电子产品类别中共有 6 种不同的产品。
# 场景 2: 排序和筛选
query = "列出价格(price)最高的5个产品,显示名称和价格"
print(f"\n📝 查询: {query}")

response = query_engine.query(query)
print(f"💬 回答: {response}")

3.2. 进阶:复杂查询与性能监控

在生产环境中需要:

class AdvancedPandasQueryEngine:
    """增强的 Pandas 查询引擎 - 带错误处理和性能监控"""
    def __init__(self, df: pd.DataFrame, llm_model: str = "gpt-4o-mini"):
        self.df = df
        self.llm = OpenAI(model=llm_model, temperature=0)
        
        # 优化的指令(中文提示效果更好)
        instruction_str = (
            "你是数据分析专家,将自然语言转换为 Pandas 代码。\n"
            "要求: \n"
            "1. 生成单行可 eval() 执行的表达式 \n"
            "2. DataFrame 变量名为 'df' \n"
            "3. 只输出代码,无其他文字 \n"
            "4. 优先使用向量化操作 \n"
        )
        
        self.query_engine = PandasQueryEngine(
            df=df, 
            verbose=True,
            instruction_str=instruction_str,
            synthesize_response=True
        )
        self.query_history = []
    
    def query(self, query_str: str, max_retries: int = 2) -> Dict[str, Any]:
        """执行查询,带重试和性能记录"""
        start_time = time.time()
        
        for attempt in range(max_retries + 1):
            try:
                response = self.query_engine.query(query_str)
                execution_time = time.time() - start_time
                
                self.query_history.append({
                    "query": query_str,
                    "time": execution_time,
                    "success": True
                })
                
                return {"success": True, "result": str(response), "time": execution_time}
                
            except Exception as e:
                if attempt == max_retries:
                    self.query_history.append({
                        "query": query_str,
                        "time": time.time() - start_time,
                        "success": False,
                        "error": str(e)
                    })
                    return {"success": False, "error": str(e)}
    
    def get_stats(self) -> Dict[str, Any]:
        """获取性能统计"""
        if not self.query_history:
            return {"message": "没有查询历史"}
        
        successful = [q for q in self.query_history if q["success"]]
        return {
            "total": len(self.query_history),
            "success": len(successful),
            "success_rate": f"{len(successful)/len(self.query_history)*100:.1f}%",
            "avg_time": f"{sum(q['time'] for q in successful)/len(successful):.2f}s" if successful else "N/A"
        }

# 初始化高级引擎
advanced_engine = AdvancedPandasQueryEngine(df)
# 进阶查询示例
advanced_queries = [
    "计算每个品牌(brand)的产品数量,按数量降序排列",
    "找出评分(rating)高于 4.5 的产品名称和价格",
    "显示各类别(category)的平均价格"
]

for query in advanced_queries:
    result = advanced_engine.query(query)
    if result["success"]:
        print(f"💬 回答: {result['result']}")
        print(f"⏱️  耗时: {result['time']:.2f}s")
    else:
        print(f"❌ 错误: {result['error']}")

# 显示统计
stats = advanced_engine.get_stats()
for k, v in stats.items():
    print(f"   {k}: {v}")

3.3. 高阶:Chain of Tables(表格思维链)

普通的 Text-to-SQL(或 Text-to-Pandas)往往试图用一句超级复杂的 SQL / 代码解决所有问题,这很容易出错(LLM 手滑写错代码)。对于复杂的多步骤分析,使用表格思维链技术:

原始表格 → [筛选] → 中间表1 → [聚合] → 中间表2 → [比较] → 最终结果

核心思想:

  1. 分解复杂问题为简单步骤
  2. 逐步执行每个操作
  3. 保留中间结果便于调试
  4. 记录操作历史可追溯
class ChainOfTablesEngine:
    """表格思维链引擎 - 多步骤推理(修正版)"""
    def __init__(self, df: pd.DataFrame):
        self.original_df = df.copy()    # 保存原始数据
        self.current_df = df.copy()    # 当前工作表
        self.operation_history = []    # 操作历史
        self.llm = Settings.llm        # LLM
    
    def filter(self, condition: str) -> pd.DataFrame:
        """筛选操作 - 真正更新 DataFrame"""
        rows_before = len(self.current_df)
        
        # 构造 prompt,让 LLM 生成筛选代码
        prompt = f"""
        你是一位 Pandas 专家。用户想要筛选数据。
        当前 DataFrame 的列名是: {list(self.current_df.columns)}
        用户的筛选条件是: {condition}

        请生成一行 Python 代码来完成筛选,要求:
        1. 变量名必须是 'df'
        2. 返回筛选后的 DataFrame
        3. 只输出代码,不要有任何解释

        示例: df[df['price'] > 5000]
        """
        
        # 调用 LLM 生成代码
        response = self.llm.complete(prompt)
        generated_code = response.text.strip()
        
        # 清理代码(去掉可能的 markdown 标记)
        if generated_code.startswith("```"):
            generated_code = generated_code.split("\n")[1]
        if generated_code.endswith("```"):
            generated_code = generated_code.rsplit("\n", 1)[0]
        generated_code = generated_code.strip()
        
        # 执行代码并更新 current_df
        try:
            df = self.current_df  # 为了代码能找到 df 变量
            filtered_df = eval(generated_code)   # 执行代码
            self.current_df = filtered_df    # 更新当前工作表
            print(f"✅ 筛选成功: {rows_before} 行 → {len(self.current_df)} 行")
        except Exception as e:
            print(f"❌ 代码执行失败: {e}")
            print(f"保持原数据不变")
        
        # 记录操作
        self.operation_history.append({
            'step': len(self.operation_history) + 1,
            'operation': 'FILTER',
            'description': condition,
            'code': generated_code,
            'rows': f"{rows_before} → {len(self.current_df)}"
        })
        
        return self.current_df
    
    def aggregate(self, aggregation: str) -> str:
        """聚合操作 - 基于当前表计算"""
        # 构造 prompt
        prompt = f"""
        你是一位 Pandas 专家。
        当前 DataFrame 有 {len(self.current_df)} 行数据,列名是: {list(self.current_df.columns)}
        用户想要: {aggregation}

        请生成一行 Python 代码来完成计算,要求:
        4. 变量名必须是 'df'
        5. 返回计算结果(数值或 Series)
        6. 只输出代码,不要解释

        示例: df['rating'].mean()
        """
        
        response = self.llm.complete(prompt)   # 调用 LLM 生成代码
        generated_code = response.text.strip()  # 去除前后的空格
        
        # 清理代码
        if generated_code.startswith("```"):
            generated_code = generated_code.split("\n")[1]
        if generated_code.endswith("```"):
            generated_code = generated_code.rsplit("\n", 1)[0]
        generated_code = generated_code.strip()
        
        # 执行计算
        try:
            df = self.current_df
            result = eval(generated_code)
            print(f"✅ 计算结果: {result}")
        except Exception as e:
            result = f"计算失败: {e}"
            print(f"❌ {result}")
        
        # 记录操作
        self.operation_history.append({
            'step': len(self.operation_history) + 1,
            'operation': 'AGGREGATE',
            'description': aggregation,
            'code': generated_code,
            'result': str(result)[:100]
        })
        
        return str(result)
    
    def reset(self):
        """重置到原始表"""
        self.current_df = self.original_df.copy()
        self.operation_history = []
    
    def show_history(self):
        """显示操作历史"""
        for op in self.operation_history:
            print(f"  步骤 {op['step']}: {op['operation']}")
            print(f"    描述: {op['description']}")
            print(f"    代码: {op.get('code', 'N/A')}")
            if 'rows' in op:
                print(f"    行数变化: {op['rows']}")
            if 'result' in op:
                print(f"    结果: {op['result']}")
            print()

# 重新初始化
cot_engine = ChainOfTablesEngine(df)
# 场景: 分析高端产品
print("\n📌 任务: 找出高价产品的平均评分")
print("思维链: 筛选(价格>5000) → 聚合(平均评分)")

# 步骤 1: 筛选
cot_engine.filter("价格 (price) 大于 5000")
# 步骤 2: 聚合
result = cot_engine.aggregate("计算评分 (rating) 的平均值")
print(f"\n🎯 最终结果: {result}")

# 显示完整历史
cot_engine.show_history()

# 重置并执行另一个分析
print("📌 任务: 各类别产品数量排名")
print("思维链: 分组 → 计数 → 排序")

cot_engine.reset()
result = cot_engine.aggregate("按类别 (category) 分组,计算每个类别的产品数量,按降序排列")
cot_engine.show_history()
# 可以通过 matplotlib 来对生成的 Pandas 数据框进行可视化
import matplotlib.pyplot as plt
import platform

# 1. 解决中文显示问题
system_name = platform.system()
if system_name == 'Darwin':  # macOS
    plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
elif system_name == 'Windows': # Windows
    plt.rcParams['font.sans-serif'] = ['SimHei']
else: # Linux
    plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei']
plt.rcParams['axes.unicode_minus'] = False 

# 2. 定义标题
title = '各类别产品数量分布'

# 3. 绘图
plt.figure(figsize=(10, 6))
ax = query_result.plot(kind='bar', color='steelblue')
plt.title(title)
plt.xlabel('类别')
plt.ylabel('数量')
plt.xticks(rotation=45, ha='right')

# 4. 添加数值标签
for p in ax.patches:
    ax.annotate(str(p.get_height()), (p.get_x() + p.get_width() / 2., p.get_height()),
                ha='center', va='center', xytext=(0, 5), textcoords='offset points')

plt.tight_layout()
plt.show()

3.4. PandasQueryEngine 总结

1. 适用场景对比

数据规模 推荐方案 原因
< 1 万行 PandasQueryEngine 零延迟、精准计算
1 - 10 万行 Text-to-SQL 数据库优化、索引支持
> 10 万行 Hybrid Retrieval 向量检索 + 过滤

2. 优化技巧

  1. 指令工程: 提供清晰的 instruction_str 指导 LLM
  2. 错误处理: 实现重试机制应对 LLM 偶发错误
  3. 性能监控: 记录查询耗时和成功率
  4. 复杂问题: 使用 Chain of Tables 分解为简单步骤

3. 局限性


四:向量检索 + BM25 基准

4.1. 方案原理

在处理结构化数据(如数据库表、Excel、CSV)的 RAG 场景中,我们将每一行数据视为一个独立的文档进行索引。然而,结构化数据具有其独特性:它既包含需要语义理解的非结构化字段(如“商品描述”、“用户评论”),也包含需要精确匹配的结构化字段(如“SKU 编码”、“具体参数”、“年份”)。面对这种混合特征,向量检索 + BM25 的组合拳是目前业界在语义检索方面的最佳实践方案。

1. 核心原理:行数据的双路召回

该方案首先将结构化表格的每一行数据进行序列化(Serialization),转化为包含字段名和字段值的文本段落(例如将 {'Name': 'iPhone 15', 'Price': 7999} 转换为 "Name: iPhone 15, Price: 7999")。基于转换后的文本,系统同时维护两套索引:

Pasted image 20260730155824.png

2. 方案 1:表格行级向量化

# 示例:向量检索实现
from llama_index.core import VectorStoreIndex, Document
import pandas as pd
from sqlalchemy import create_engine

# 数据库连接
db_url = "sqlite:///dataset/ecommerce.db"
engine = create_engine(db_url)

# 加载数据
df = pd.read_sql("SELECT * FROM products LIMIT 15", engine)

# 将每行转为文本
documents = []
for _, row in df.iterrows():
    text = f"""
    产品名称: {row.get('name', row.get('product_name', 'Unknown'))}
    类别: {row.get('category', 'N/A')}
    价格: {row.get('price', row.get('unit_price', 0))}元
    """
    documents.append(Document(text=text, metadata=row.to_dict()))

# 创建向量索引
vector_index = VectorStoreIndex.from_documents(
    documents,
    insert_batch_size=10  # ✅ DashScope 同步接口限制:每批最多 10 条
)
vector_retriever = vector_index.as_retriever(similarity_top_k=3)

3. 方案 2:分块向量化(推荐用于大规模数据)

# 示例:分块向量化实现
from llama_index.core import VectorStoreIndex, Document
import pandas as pd
from sqlalchemy import create_engine

# 数据库连接
db_url = "sqlite:///dataset/ecommerce.db"
engine = create_engine(db_url)

# 加载数据
df = pd.read_sql("SELECT * FROM orders", engine)

# ========== 分块参数 ==========
CHUNK_SIZE = 10  # 每个分块包含的行数

# 将多行合并为一个文档(分块向量化)
documents = []
for chunk_start in range(0, len(df), CHUNK_SIZE):
    # 取出当前分块的数据
    chunk_df = df.iloc[chunk_start:chunk_start + CHUNK_SIZE]
    
    # 将分块内所有行拼接为文本
    text_parts = []
    for _, row in chunk_df.iterrows():
        item = f"产品id: {row.get('product_id')} | 用户id: {row.get('user_id')} | 数量: {row.get('quantity')} | 订单日期: {row.get('order_date')} | 订单金额: {row.get('amount')}元 | 订单状态: {row.get('status')} "
        text_parts.append(item)
    
    # 合并为单个文档
    chunk_text = "\n".join(text_parts)
    
    # 元数据记录分块信息
    metadata = {
        "chunk_id": chunk_start // CHUNK_SIZE,
        "row_range": f"{chunk_start}-{min(chunk_start + CHUNK_SIZE, len(df))}",
        "status": list(chunk_df['status'].unique())  # 分块包含的状态
    }
    
    documents.append(Document(text=chunk_text, metadata=metadata))

print(f"生成 {len(documents)} 个分块文档 (每块 {CHUNK_SIZE} 行)")

# 创建向量索引
vector_index = VectorStoreIndex.from_documents(
    documents,
    insert_batch_size=10  # DashScope 限制
)
vector_retriever = vector_index.as_retriever(similarity_top_k=3)

4. 方案 3:混合索引(最灵活)

4.2. Milvus 向量数据库存储 + BM25 检索

"""
Milvus 混合向量化示例

核心优势:
Milvus 原生支持元数据(标量字段)过滤
性能优异,适合大规模生产环境
支持本地 Lite 模式(./milvus.db)和服务器模式
"""
from typing import Optional, Dict, Any, List, Tuple, Union
import pandas as pd
import numpy as np
from sqlalchemy import create_engine
from tqdm import tqdm
from llama_index.core import (
    VectorStoreIndex,
    Document,
    StorageContext,
    Settings
)
from llama_index.core.vector_stores import (
    MetadataFilters,
    MetadataFilter,
    FilterOperator
)
from llama_index.vector_stores.milvus import MilvusVectorStore
from llama_index.embeddings.dashscope import DashScopeEmbedding
import os
from dotenv import load_dotenv
from sqlalchemy import inspect

# 加载环境变量
load_dotenv()

class UniversalHybridVectorizer:
    """
    通用混合索引 - 使用 Milvus 支持元数据过滤
    
    核心功能:
    1. 支持 CSV / Excel / 数据库多种数据源
    2. 数据库自动推断字段:利用 sqlalchemy.inspect 扫描表结构,自动将字段归类:
            长文本(Text / LongText):自动划为 text_fields,用于生成向量,提供语义搜索能力。
            数值 / 日期(Int / Float / Date):自动划为 metadata_fields,用于范围查询(如“价格 > 5000”)。
            短文本(Tags / Category):自动划为 metadata_fields,用于精确匹配(如“品牌=Apple”)。
    3. 使用 Milvus 实现高性能向量检索 + 标量过滤
    """
    def __init__(
        self, 
        db_url: Optional[str] = None,
        table: Optional[str] = None,
        # ===== 可选:手动覆盖自动推断 =====
        text_fields: Optional[List[str]] = None,
        metadata_fields: Optional[List[str]] = None,
        id_field: Optional[str] = None,
        numeric_fields: Optional[List[str]] = None,
        categorical_fields: Optional[List[str]] = None
    ):
        """
        初始化向量化器
        方式 1(推荐):传入数据库连接,自动推断字段
            vectorizer = UniversalHybridVectorizer(
                db_url="sqlite:///ecommerce.db",
                table="products"
            )
        方式 2:手动指定所有字段(用于 CSV / Excel或需要精确控制)
            vectorizer = UniversalHybridVectorizer(
                text_fields=["name", "description"],
                metadata_fields=["price", "category"]
            )
        Args:
            db_url: 数据库连接字符串(可选)
            table: 表名,用于自动推断字段(可选)
            text_fields: 需要向量化的字段列表
            metadata_fields: 作为元数据的字段列表
            id_field: 唯一标识符字段名
            numeric_fields: 数值字段列表(用于范围查询)
            categorical_fields: 类别字段列表(用于精确 / IN 查询)
        """
        self.engine = None
        self.table = table
        
        # 如果提供了数据库连接,创建引擎
        if db_url:
            self.engine = create_engine(db_url)
        
        # 自动推断字段(如果提供了 db_url 和 table 且未手动指定字段)
        if self.engine and table and not text_fields:
            auto_fields = self._auto_detect_fields(table)
            self.text_fields = text_fields or auto_fields.get('text', [])
            self.metadata_fields = metadata_fields or auto_fields.get('metadata', [])
            self.id_field = id_field or auto_fields.get('id')
            self.numeric_fields = numeric_fields or auto_fields.get('numeric', [])
            self.categorical_fields = categorical_fields or auto_fields.get('categorical', [])
            
            print(f"📊 自动推断字段配置:")
            print(f"   文本字段: {self.text_fields}")
            print(f"   数值字段: {self.numeric_fields}")
            print(f"   分类字段: {self.categorical_fields}")
            print(f"   ID 字段: {self.id_field}")
        else:
            # 使用手动指定的字段
            self.text_fields = text_fields or []
            self.metadata_fields = metadata_fields or []
            self.id_field = id_field
            self.numeric_fields = numeric_fields or []
            self.categorical_fields = categorical_fields or []
        
        self.index = None
    
    def _auto_detect_fields(self, table: str) -> Dict[str, Any]:
        """
        从数据库表结构自动推断字段分类
        
        分类规则:
        - TEXT, LONGTEXT, VARCHAR(255+) → text_fields
        - INTEGER, FLOAT, DECIMAL → numeric_fields + metadata_fields
        - VARCHAR(<255) → categorical_fields + metadata_fields
        - PRIMARY KEY → id_field
        """
        # 获取表结构
        inspector = inspect(self.engine)
        # 获取表的列信息
        columns = inspector.get_columns(table)
        # 获取主键约束
        pk_constraint = inspector.get_pk_constraint(table)
        result = {
            'text': [],
            'metadata': [],
            'numeric': [],
            'categorical': [],
            'id': None
        }
        
        # 获取主键
        if pk_constraint and pk_constraint.get('constrained_columns'):
            result['id'] = pk_constraint['constrained_columns'][0]
        
        for col in columns:
            col_name = col['name']
            col_type = str(col['type']).upper()
            
            # 跳过主键(已单独处理)
            if col_name == result['id']:
                continue
            
            # 根据类型分类
            if 'TEXT' in col_type or 'LONGTEXT' in col_type:
                result['text'].append(col_name)
            elif 'VARCHAR' in col_type:
                # 解析 VARCHAR 长度
                import re
                match = re.search(r'VARCHAR\((\d+)\)', col_type)
                length = int(match.group(1)) if match else 50
                
                if length >= 255:
                    result['text'].append(col_name)
                else:
                    result['categorical'].append(col_name)
                    result['metadata'].append(col_name)
            elif any(t in col_type for t in ['INT', 'FLOAT', 'DECIMAL', 'NUMERIC', 'REAL']):
                result['numeric'].append(col_name)
                result['metadata'].append(col_name)
            elif any(t in col_type for t in ['DATE', 'TIME', 'DATETIME', 'TIMESTAMP']):
                result['metadata'].append(col_name)
            else:
                # 其他类型默认加入 metadata
                result['metadata'].append(col_name)
        
        return result
    
    def load_from_database(
        self,
        table: Optional[str] = None,
        query: Optional[str] = None,
        limit: Optional[int] = None,
        chunk_size: int = 1000
    ) -> List[Document]:
        """
        从数据库加载数据
        Args:
            table: 表名(默认使用初始化时的表)
            query: 自定义 SQL 查询(支持 JOIN,优先于 table)
            limit: 限制返回行数
            chunk_size: 分批读取的行数
        Examples:
            # 简单用法:加载初始化时指定的表
            docs = vectorizer.load_from_database()
            # 指定表和限制
            docs = vectorizer.load_from_database(table="products", limit=1000)
            # 自定义 SQL(支持 JOIN)
            docs = vectorizer.load_from_database(
                query=\"\"\"
                    SELECT p.*, r.content as review_content 
                    FROM products p 
                    LEFT JOIN reviews r ON p.id = r.product_id
                \"\"\"
            )
        """
        if not self.engine:
            raise ValueError("未配置数据库连接,请在初始化时传入 db_url")
        
        # 确定要使用的表
        target_table = table or self.table
        
        # 构建SQL
        if query:
            sql = query
            # 检测查询中的新列并尝试自动推断
            self._detect_new_columns_from_query(query)
        elif target_table:
            sql = f"SELECT * FROM {target_table}"
            if limit:
                sql += f" LIMIT {limit}"
            print(f"🗄️  加载表: {target_table}" + (f" (LIMIT {limit})" if limit else ""))
        else:
            raise ValueError("必须指定 table 或 query")
        
        # 分批读取
        all_documents = []
        chunks = pd.read_sql(sql, self.engine, chunksize=chunk_size)
        
        for df_chunk in tqdm(chunks, desc="处理数据库数据"):
            documents = self._convert_to_hybrid_documents(df_chunk)
            all_documents.extend(documents)
        
        print(f"✅ 生成 {len(all_documents)} 个文档")
        return all_documents
    
    def _convert_to_hybrid_documents(self, df: pd.DataFrame) -> List[Document]:
        """
        “结构化”转“非结构化”的关键中间层,转换为混合文档
        策略:
        1. 文本化(Text Parts):将所有 text_fields 的内容拼接成一段自然语言描述(例如:"name: iPhone 15 \n description: 最新款苹果手机..."),
            这部分会被 Embedding 模型转化为向量。
        2. 元数据化(Metadata):将所有 metadata_fields(如价格、类别、库存)保留为原始键值对(Key-Value),这部分会被存入 Milvus 的标量字段中,
            用于后续的 Filter 操作。
        这实现了**“语义在向量里,属性在字段里”**的双重存储结构。
        """
        documents = []
        
        for idx, row in df.iterrows():
            # 1. 构建向量化文本
            text_parts = []
            for field in self.text_fields:
                if field in row and pd.notna(row[field]):
                    text_parts.append(f"{field}: {row[field]}")
            
            text = "\n".join(text_parts) if text_parts else "N/A"
            
            # 2. 构建元数据
            metadata = {}
            for field in self.metadata_fields:
                if field in row and pd.notna(row[field]):
                    value = row[field]
                    # 处理 pandas 特殊类型
                    if isinstance(value, (pd.Timestamp, pd.Timedelta)):
                        value = str(value)
                    metadata[field] = value
            
            # 3. 确定文档 ID
            if self.id_field and self.id_field in row:
                doc_id = str(row[self.id_field])
            else:
                doc_id = str(idx)
            
            # 4. 创建文档
            doc = Document(
                text=text,
                metadata=metadata,
                doc_id=doc_id
            )
            documents.append(doc)
        
        return documents
    
    def build_index(self, documents: List[Document], 
                      save_path: str = "./hybrid_index",
                      collection_name: str = "hybrid_collection",
                      milvus_uri: str = "./milvus_demo.db"):
        """
        构建混合索引(使用 Milvus 支持元数据过滤)
        Args:
            documents: 文档列表
            save_path: 索引保存路径(兼容参数,实际数据存储在 Milvus)
            collection_name: Milvus 集合名称
            milvus_uri: Milvus 连接 URI
                - 本地模式: "./milvus_demo.db" (使用 Milvus Lite)
                - 服务器模式: "http://localhost:19530"
        """
        from llama_index.vector_stores.milvus import MilvusVectorStore
        
        # 使用 Milvus 构建向量存储(支持元数据过滤!)
        # dim=1024 对应 DashScope text-embedding-v4 的向量维度
        vector_store = MilvusVectorStore(
            uri=milvus_uri,            # Milvus 连接 URI
            collection_name=collection_name,  # Milvus 集合名称
            dim=1024,          # 向量维度,最好跟 Embedding 模型长度一致
            overwrite=True  # 如果存在同名 collection 则覆盖
        )
        
        # 使用 MilvusVectorStore 构建存储上下文
        storage_context = StorageContext.from_defaults(
            vector_store=vector_store
        )
        
        # 使用 VectorStoreIndex 构建向量索引
        self.index = VectorStoreIndex.from_documents(
            documents,
            storage_context=storage_context,  # 使用 MilvusVectorStore
            show_progress=True,   # 显示进度
            insert_batch_size=10  # ✅ DashScope 同步接口限制:每批最多 10 条
        )
        
        # Milvus 数据是自托管的,不需要 persist 到文件系统
        print(f"✅ 索引已创建: collection={collection_name}, uri={milvus_uri}")
        
        vector_retriever = self.index.as_retriever(similarity_top_k=5)
        return vector_retriever
# 1. 创建向量化器
vectorizer = UniversalHybridVectorizer(
        db_url="sqlite:///dataset/ecommerce.db",
        table="products"
    )
# 自动打印:文本字段: ['description'], 数值字段: ['price', 'stock']...

# 2. 加载数据
docs = vectorizer.load_from_database()  # 使用初始化时的表

# 3. 构建 Milvus 检索器
vector_retriever = vectorizer.build_index(
    docs, 
    collection_name="products",
    milvus_uri="http://localhost:19530"
)

1. BM25 检索器

在本项目中,BM25 检索器的实现承担着能够“精确匹配关键词”的重要角色。其实现逻辑由底层的核心算法与上层的过滤增强两个层面构成,采用了一种典型的“后过滤”(Post-Filtering)架构。

核心算法原理(Standard BM25) LlamaIndex 的 BM25Retriever 基于经典的概率检索模型(通常封装 rank_bm25 库)。它在内存中工作,主要流程如下:

目前主流 BM25 检索器框架对比

维度 Elasticsearch LlamaIndex BM25 Whoosh
核心定位 企业级分布式搜索引擎业界标准的全文检索引擎,功能极其强大且成熟。 RAG 专用轻量检索器 LlamaIndex 框架内置组件,专为混合检索(Hybrid Search)设计。 Python 原生轻量检索引擎纯 Python 实现的独立搜索库,无任何外部依赖。
部署架构 独立服务 (Server) 需要部署独立的 ES 集群(Java),资源消耗大,运维成本高。 内存 / 本地运行 (In-Memory) 通常基于内存(rank_bm25)或本地文件,作为 Python 进程的一部分运行。 本地库 (Library) 纯 Python 库,索引文件存储在本地磁盘,即插即用,无需独立进程。
BM25 实现 高度优化基于 Lucene 内核,性能极高,支持分词、停用词、同义词等复杂 NLP 处理。 简化版 / 包装器通常封装了 rank_bm25 库,功能相对基础,主要依赖前置的分词处理。 可定制版内置了 BM25F 等变种算法,支持 Python 扩展评分逻辑,灵活性较好。
扩展性 ⭐⭐⭐⭐⭐ (极强)支持 PB 级海量数据,分布式横向扩展,高可用。 ⭐⭐ (较弱)受限于单机内存或简单的文件存储,适合中小规模数据(如几万条文档)。 ⭐⭐ (一般)单机文件存储,数据量过大时(如百万级)性能下降明显,不支持分布式。
上手难度 ⭐⭐⭐ (较高)由于是独立服务,需要学习 DSL 查询语法、索引配置和集群运维。 ⭐ (极低)代码集成简单,几行 Python 代码即可初始化,与 RAG 流程无缝衔接。 ⭐⭐ (中等)需要定义 Schema 和编写索引逻辑,比 ES 简单但比 LlamaIndex 繁琐。
核心优势 生产级稳定性与生态拥有 Kibana 可视化、丰富的插件生态,适合对检索要求极高的核心业务。 RAG 集成度最高天生适配 Vector Store,一行代码实现“向量 + 关键词”混合检索。 纯 Python 零依赖无需安装 Java 或 Docker,非常适合嵌入式环境或纯 Python 轻量应用。
适用场景 1. 海量数据(千万 / 亿级)的企业知识库。2. 需要高并发、高可用支持的生产环境。3. 已有 ES 基础设施的复用。 1. RAG 快速原型开发。2. 数据量较小(< 10 万文档)的垂直领域知识库。3. 极速验证混合检索效果。 1. 单机 / 桌面应用(如本地文档搜索工具)。2. 无法额外部署重型服务(ES)的受限环境。3. 中小规模数据的纯 Python 项目。
from llama_index.core.schema import TextNode
from llama_index.retrievers.bm25 import BM25Retriever

import pandas as pd
from sqlalchemy import create_engine

# 数据库连接
db_url = "sqlite:///dataset/ecommerce.db"
engine = create_engine(db_url)

# 加载 products 表数据
df = pd.read_sql("SELECT * FROM products", engine)
print(f"总数据量: {len(df)}行")

# 直接创建 Nodes(跳过 Documents)
nodes = []
for idx, row in df.iterrows():
    # 将所有字段都添加到 text 中
    text = f"""产品 ID: {row.get('product_id')}
            产品名称: {row.get('name')}
            类别: {row.get('category')}
            价格: {row.get('price')} 元
            库存: {row.get('stock')} 件
            评分: {row.get('rating')}
            描述: {row.get('description')}"""

    node = TextNode(
        text=text,
        metadata=row.to_dict(),
        id_=f"product_{idx}"
    )
    nodes.append(node)

# 创建 BM25 检索器
bm25_retriever = BM25Retriever.from_defaults(
    nodes=nodes,
    similarity_top_k=5
)

2. 创建 QueryFusionRetriever 混合检索器

from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.core.query_engine import RetrieverQueryEngine

# 1. 创建 QueryFusionRetriever 混合检索器
retriever = QueryFusionRetriever(
    retrievers=[vector_retriever, bm25_retriever],  # 集成不同的检索器
    similarity_top_k=5,                 # 最终返回的文档数量
    num_queries=1,                      # 为原始查询生成的变体数量,(默认为 1,不生成变体)
    mode="reciprocal_rerank",           # 结果融合模式,'reciprocal_rerank' 是常用且效果好的模式
    use_async=False,                     # 是否异步执行,Jupyter Notebook 环境做测试不用开启异步
    verbose=True,                       # 是否打印调试信息
    retriever_weights=[0.8,0.2],        # 检索器权重,用于加权融合
)

# 2.构建 QueryEngine,查询引擎
retriever_query_engine = RetrieverQueryEngine.from_args(
    retriever=retriever
)

# 3. 构建测试问题
test_queries = [
    "适合办公的笔记本 (价格 5000 - 15000 元)",
    "适合办公的电子产品",    # 语义查询
    "价格在 1000 元以内的产品",  # 数值范围查询,
    "所有产品的平均价格是多少?", # 聚合查询
]

for query in test_queries:
    response = retriever_query_engine.query(query)
    print(f"回答: {response}")
    print("-" * 80)

3. 实现 rerank 重排序检索

from llama_index.core.postprocessor import SentenceTransformerRerank
from llama_index.core.query_engine import RetrieverQueryEngine

# 模型本地路径
local_model_path = "./models/bge-reranker-v2-m3"

# 1. 定义精排序器 (Reranker)
reranker = SentenceTransformerRerank(
    model=local_model_path,   # 指定模型路径
    top_n=3, # 精选出最相关的 4 个文档送给 LLM,
)

# 2.构建 QueryEngine
query_engine = RetrieverQueryEngine.from_args(
    retriever=retriever,
    node_postprocessors=[reranker]  # 添加精排序器
)

# 3. 执行查询
query = "请帮我查询适合办公的笔记本 (价格 5000 - 15000 元)有哪些?请把产品名称和价格都展示出来"
response = query_engine.query(query)

性能评估

测试结果 (基于 2 个测试查询):

查询类型 准确率 示例查询 问题
精确数值 25% ❌ "价格 1000 元以内的产品" 无法精确筛选范围
聚合统计 10% ❌ "平均价格" 无法计算 SUM / AVG
语义理解 88% ✅ "适合办公" 语义匹配良好
整体 45% - 不适合精确查询

核心问题:

4.3. Attu Milvus 官方 GUI 管理工具

Pasted image 20260730162312.png


五:Text-to-SQL 基础

5.1. 方案原理

Text-to-SQL 是一种将自然语言自动转换为结构化查询语言(SQL)的技术方案。在处理结构化数据的 RAG 系统中,它是解决“精确统计”与“复杂逻辑计算”问题的终极武器,其本质是将 LLM 从“阅读者”升级为“操作者”。

1. 核心原理:语义到代码的精准翻译

不同于向量检索基于“语义相似度”的模糊匹配模式,Text-to-SQL 采用的是逻辑推理 + 代码生成模式。其核心在于利用 LLM 强大的理解力,将由人类发出的模糊指令(如“统计上季度销售额增长超过 20% 的部门”)翻译成数据库能够严格执行的标准 SQL 语句。在这个过程中,LLM 充当了一个高智商的中间层,它不仅要理解用户的意图,还要理解数据库的表结构(Schema)和表之间的关联关系(Relationship)。


用户查询 → LLM 生成 SQL → 数据库执行 → 结果 → LLM 生成答案

大概流程如下

Pasted image 20260807092344.png

2. 实现思路与关键流程

一个成熟的 Text-to-SQL 工作流通常包含以下四个核心步骤:

3. 核心优势与场景

Text-to-SQL 的最大价值在于计算的精确性。向量检索无法准确回答“所有产品的平均价格是多少”或“库存少于 10 的产品有哪些”这类问题,而 SQL 引擎天生就是为此设计的。因此,该方案特别适用于财务报表分析、业务数据统计、库存管理查询等对数值精度和聚合逻辑有严格要求的场景。

Text-to-SQL 主流工具对比分析:

维度 LlamaIndex LangChain Vanna AI
核心定位 数据索引与检索专家专注于连接 LLM 与外部数据,提供高效的上下文索引能力。 全能型 LLM 编排框架提供最广泛的组件和工具链,用于构建复杂的应用逻辑。 Text-to-SQL 垂直领域专家专为“自然语言转 SQL”场景设计的 RAG 框架。
SQL 生成方式 基于检索增强 (RAG) 擅长先检索相关 Schema / Table 元数据,再生成 SQL,适合大规模 Schema。 基于链式调用 (Chain) 通过 SQLDatabaseChain 等组件组合 Prompt 和执行逻辑,灵活性极高。 基于训练 / 反馈 (Train / RAG) 内置了专门针对 SQL 优化的 RAG 流程,且具备“训练”能力(基于历史问答优化)。
准确率优化 ✅ 检索精度高通过 ObjectRetriever 等机制,能从海量表中精准定位相关表,减少 token 消耗。 ⚠️ 依赖 Prompt 工程需要开发者自行设计 Few-shot 样本和优化 Prompt 策略来提升效果。 🚀 准确率极高自动将 Schema、DDL、文档和历史正确 SQL 向量化,越用越准。
上手难度 ⭐⭐ (中等)概念较多(Index, Retriever),但对数据层的封装非常完善。 ⭐⭐⭐ (较高)组件极其丰富,定制化开发需要较强的代码和逻辑编排能力。 ⭐ (极低) API 设计简洁,几乎开箱即用,只需几行代码即可连接数据库并提问。
灵活性 ⭐⭐⭐ 数据接入灵活,检索策略可定制。 ⭐⭐⭐⭐⭐ 逻辑、Prompt、工具调用完全由开发者掌控,可实现极复杂的 Agent。 ⭐⭐ 专注 SQL 生成,流程相对固定,但通过配置也能适配多种 LLM 和向量库。
核心优势 Token 效率高、适合大表在几百张表的数据库中,能快速检索出最相关的几张表给 LLM,避免上下文超长。 生态丰富、上限高由于是通用框架,可以轻松结合其他非 SQL 工具(如搜索、计算器)构建综合 Agent。 自带前端与可视化内置了 Streamlit / Flask UI,甚至支持生成 Plotly 图表,交付速度最快。
适用场景 1. 表数量巨大的企业级数仓。2. 需要结合非结构化文档(如 PDF 合同查数据)的混合检索场景。 1. 需要高度定制业务逻辑(如审批流、多跳推理)。2. 已有 LangChain 生态的项目集成。 1. 快速验证 Text-to-SQL 可行性。2. 需要高准确率且表结构相对稳定的生产环境。3. 需快速交付带可视化界面的 Demo。

5.2. 创建测试数据库

我们创建一个电商数据库,包含:

数据保存在 ecommerce.db 文件中,可以自行查看

# 创建电商数据库的完整代码会非常长
# 这里提供核心创建逻辑,完整版见项目文件
import sqlite3
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import random
from pathlib import Path
from sqlalchemy import create_engine, text
from llama_index.core import SQLDatabase, Settings

# 使用已有的测试数据库作为示例
db_path = "../large_data_test.db"
# 数据库文件路径
db_path = Path("dataset/ecommerce.db")

if not db_path.exists():
    print("❌ 数据库文件不存在!")
    print("请先运行: python init_database.py")
else:
    # 创建 SQLAlchemy 引擎
    engine = create_engine(f"sqlite:///{db_path}")
    # 创建 LlamaIndex SQLDatabase 对象
    sql_database = SQLDatabase(engine)
    print(f"   数据库路径: {db_path.absolute()}")
print("✅ 数据库创建完成")

1. 查看示例数据

# 数据库相关
import sqlite3
# 工具库
import pandas as pd
from rich import print as rprint
from rich.console import Console
from rich.table import Table

console = Console()

# 查看用户表前 5 条数据
with engine.connect() as conn:
    result = conn.execute(text("SELECT * FROM users LIMIT 5"))
    df_users = pd.DataFrame(result.fetchall(), columns=result.keys())

display(df_users)

# 查看产品表前 5 条数据
with engine.connect() as conn:
    result = conn.execute(text("SELECT * FROM products LIMIT 5"))
    df_products = pd.DataFrame(result.fetchall(), columns=result.keys())

display(df_products)

2. 创建 SQL 查询引擎

# Text-to-SQL 实现
from llama_index.core import SQLDatabase
from llama_index.core.query_engine import NLSQLTableQueryEngine
from sqlalchemy import create_engine

# 创建 SQL 数据库连接
engine = create_engine("sqlite:///dataset/ecommerce.db")
sql_database = SQLDatabase(engine, include_tables=["products", "orders", "users","reviews"])

# 创建 SQL 查询引擎
sql_query_engine = NLSQLTableQueryEngine(
    sql_database=sql_database,
    synthesize_response=True
)

3. 查看数据库表结构

# 获取所有表名
table_names = sql_database.get_usable_table_names()

for table in table_names:
    console.print(f"  • {table}")

# 查看每个表的结构
for table in table_names:
    console.print(f"\n[yellow]{table}[/yellow]:")
    schema = sql_database.get_single_table_info(table)
    console.print(schema)

5.3. 性能评估

测试结果:

查询类型 向量检索 Text-to-SQL 提升
精确数值 25% 85% ✅ +60%
聚合统计 10% 95% ✅ +85%
语义理解 88% 40% ❌ -48%
整体 45% 75% +30%

核心发现:


六:Text-to-SQL 优化

6.1. 优化技术概览

我们将应用 4 种优化技术:

  1. Schema 增强 - 添加表和字段的详细描述
  2. Few-shot 示例 - 提供高质量查询示例
  3. SQL 验证 - 自动检测和修复错误
  4. 查询缓存 - 减少重复 LLM 调用

预期提升: 75% → 92%

6.2. Schema 增强技术

1. 为什么需要 Schema 增强?

基础的表结构信息可能不足以让 LLM 准确理解数据含义。通过添加:

可以显著提升 SQL 生成的准确性。

2. 创建增强的 Schema

from llama_index.core.objects import SQLTableSchema

# 为每个表创建增强的 Schema
table_schemas = [
    SQLTableSchema(
        table_name="users",
        context_str="""
        用户表,存储所有注册用户的基本信息。
        
        字段说明:
        - user_id: 用户唯一标识符
        - name: 用户姓名
        - email: 用户邮箱地址
        - region: 用户所在地区(如:北京、上海、广州等)
        - register_date: 用户注册日期
        - vip_level: VIP 等级(0=普通用户, 1-3=不同等级 VIP)
        - total_spent: 用户累计消费金额(单位:元)
        
        业务规则:
        - VIP 等级越高,享受的折扣越多
        - total_spent 会随着订单完成自动更新
        """
    ),
    SQLTableSchema(
        table_name="products",
        context_str="""
        产品表,存储所有可售卖的商品信息。
        
        字段说明:
        - product_id: 产品唯一标识符
        - name: 产品名称
        - category: 产品分类(电子产品、家用电器、服装鞋帽、家具等)
        - price: 产品价格(单位:元)
        - stock: 库存数量
        - rating: 产品评分(1-5 分,保留两位小数)
        - description: 产品详细描述
        
        业务规则:
        - rating 基于用户评论自动计算
        - stock 为 0 时产品不可购买
        """
    ),
    SQLTableSchema(
        table_name="orders",
        context_str="""
        订单表,记录所有用户的购买订单。
        
        字段说明:
        - order_id: 订单唯一标识符
        - user_id: 下单用户 ID(外键关联 users 表)
        - product_id: 购买产品 ID(外键关联 products 表)
        - quantity: 购买数量
        - amount: 订单总金额(单位:元)= price * quantity
        - order_date: 下单日期
        - status: 订单状态(已完成、配送中、已取消等)
        
        业务规则:
        - 只有状态为"已完成"的订单才计入销售额
        - amount 已包含所有折扣
        """
    ),
    SQLTableSchema(
        table_name="reviews",
        context_str="""
        评论表,存储用户对产品的评价。
        
        字段说明:
        - review_id: 评论唯一标识符
        - user_id: 评论用户 ID(外键关联 users 表)
        - product_id: 被评论产品 ID(外键关联 products 表)
        - rating: 评分(1-5 分整数)
        - comment: 评论内容
        - review_date: 评论日期
        
        业务规则:
        - 用户必须购买过产品才能评论
        - rating 影响产品的总体评分
        """
    )
]

3. 使用增强 Schema 创建查询引擎

# 创建带有增强 Schema 的查询引擎
enhanced_query_engine = NLSQLTableQueryEngine(
    sql_database=sql_database,      # SQL 数据库对象
    tables=["users", "products", "orders", "reviews"], # 指定可供查询的表列表
    table_schemas=table_schemas,    # 注入包含列描述的增强 Schema
    verbose=True,                   # 输出详细的 SQL 生成过程
    synthesize_response=True        # 将 SQL 执行结果转换为自然语言回复
)

4. 对比测试:基础 vs 增强

测试查询 1: 复杂业务查询

query = "哪些 VIP 用户的累计消费超过 10000 元?"

# 基础引擎
try:
    response_basic = sql_query_engine.query(query)
    console.print(f"回答: {response_basic}")
    console.print(f"SQL: {response_basic.metadata.get('sql_query', 'N/A')}")
except Exception as e:
    console.print(f"[red] 错误: {e}[/red]")

# 增强引擎
try:
    response_enhanced = enhanced_query_engine.query(query)
    console.print(f"回答: {response_enhanced}")
    console.print(f"SQL: {response_enhanced.metadata.get('sql_query', 'N/A')}")
except Exception as e:
    console.print(f"[red] 错误: {e}[/red]")

测试查询 2: 需要理解业务规则

query = "计算已完成订单的总销售额"

# 增强引擎(应该能正确理解"已完成"状态)
response = enhanced_query_engine.query(query)

6.3. Few-shot 示例优化

1. 什么是 Few-shot Learning?